// ==UserScript== // @name GitHub Commit Sort // @namespace xiaohuohumax/userscripts/github-commit-sort // @version 1.1.0 // @author xiaohuohumax // @description GitHub 仓库文件按提交时间排序 // @license MIT // @icon https://github.githubassets.com/favicons/favicon-dark.png // @source https://github.com/xiaohuohumax/userscripts.git // @match https://github.com/* // @grant GM_addValueChangeListener // @grant GM_getValue // @grant GM_setValue // @noframes // ==/UserScript== (function () { 'use strict'; var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); const debounce = ({ delay }, func) => { let timer = void 0; let active = true; const debounced = (...args) => { if (active) { clearTimeout(timer); timer = setTimeout(() => { active && func(...args); timer = void 0; }, delay); } else { func(...args); } }; debounced.isPending = () => { return timer !== void 0; }; debounced.cancel = () => { active = false; }; debounced.flush = (...args) => func(...args); return debounced; }; const version = "1.1.0"; var _GM_addValueChangeListener = /* @__PURE__ */ (() => typeof GM_addValueChangeListener != "undefined" ? GM_addValueChangeListener : void 0)(); var _GM_getValue = /* @__PURE__ */ (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)(); var _GM_setValue = /* @__PURE__ */ (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)(); class Store { constructor() { __publicField(this, "state", "default"); __publicField(this, "listeners", []); __publicField(this, "SORT_ICON_MAP", { desc: "▴", asc: "▾", default: "=" }); this.state = _GM_getValue("state", "default"); _GM_addValueChangeListener("state", (_key, _oldValue, newValue, remote) => { if (remote) { this.state = newValue; this.listeners.forEach((listener) => listener(this.state)); } }); } addChangeListener(listener) { this.listeners.push(listener); } toggleState() { const states = Object.keys(this.SORT_ICON_MAP); this.state = states[(states.indexOf(this.state) + 1) % states.length]; _GM_setValue("state", this.state); } updateState(state) { this.state = state; _GM_setValue("state", this.state); } get sortIcon() { return this.SORT_ICON_MAP[this.state]; } } const ID = "github-commit-sort"; const VERSION = version; const SORT_BUTTON_ID = `${ID}-sort-button`; const store = new Store(); function queryTable() { return document.querySelector('table[aria-labelledby="folders-and-files"]'); } function querySortButtonParent() { const table = queryTable(); const mainTable = table == null ? void 0 : table.querySelector( 'body tr[class^="Box-sc-"] > td > div > div:last-child' ); if (mainTable) { return { sortButtonParent: mainTable, isMainPage: true }; } const treeTable = table == null ? void 0 : table.querySelector( "thead > tr > th:last-child > div" ); return { sortButtonParent: treeTable, isMainPage: false }; } function sortRowsByState(sortButton, toggle) { var _a; const tableBody = (_a = queryTable()) == null ? void 0 : _a.querySelector("tbody"); if (!tableBody) { return; } const hasSkeleton = tableBody.querySelector('div[class^="Skeleton"]'); if (hasSkeleton) { return; } const rowElements = tableBody.querySelectorAll( 'tr[class^="react-directory-row"]' ); const rows = Array.from(rowElements).map((element) => { const relativeTimeElement = element.querySelector("relative-time"); const id = element.getAttribute("id"); const datetime = relativeTimeElement.getAttribute("datetime"); return { element, date: new Date(datetime), id, index: Number(id.split("-").pop()) }; }); toggle && store.toggleState(); sortButton.textContent = store.sortIcon; const sortedRows = rows.sort((a, b) => { if (store.state === "default") { return a.index - b.index; } const aDate = a.date.getTime(); const bDate = b.date.getTime(); return store.state === "asc" ? aDate - bDate : bDate - aDate; }); const viewAllFilesElement = tableBody.querySelector( 'tr[data-testid="view-all-files-row"' ); sortedRows.forEach((row) => { row.element.remove(); viewAllFilesElement ? tableBody.insertBefore(row.element, viewAllFilesElement) : tableBody.appendChild(row.element); }); } function main(observer2) { const { sortButtonParent, isMainPage } = querySortButtonParent(); if (!sortButtonParent) { return; } observer2 && observer2.disconnect(); if (isMainPage) { sortButtonParent.style.alignItems = "center"; } else { const th = sortButtonParent.parentNode; th.style.width = "170px"; } let sortButton = document.getElementById(SORT_BUTTON_ID); if (!sortButton) { sortButton = document.createElement("button"); sortButton.textContent = store.sortIcon; sortButton.classList.add("Button", "Button--iconOnly", "Button--secondary"); sortButton.id = SORT_BUTTON_ID; sortButton.style.width = "var(--control-xsmall-size)"; sortButton.style.height = "var(--control-xsmall-size)"; sortButton.style.marginLeft = isMainPage ? "0" : "10px"; sortButtonParent.appendChild(sortButton); sortButton.addEventListener("click", () => sortRowsByState(sortButton, true)); } sortRowsByState(sortButton, false); observer2 && observer2.observe(document.body, { childList: true, subtree: true }); } console.log(`${ID}(v${VERSION})`); const mainDebounced = debounce({ delay: 500 }, main); const observer = new MutationObserver(() => mainDebounced(observer)); observer.observe(document.body, { childList: true, subtree: true }); store.addChangeListener(mainDebounced.bind(null, observer)); })();